/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2012  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu.writers;

import java.awt.Color;
import java.awt.Dimension;
import java.io.PrintWriter;
import java.util.List;

import org.sosy_lab.ccvisu.Options;
import org.sosy_lab.ccvisu.Options.OptionsEnum;
import org.sosy_lab.ccvisu.graph.GraphData;
import org.sosy_lab.ccvisu.graph.GraphEdge;
import org.sosy_lab.ccvisu.graph.GraphVertex;
import org.sosy_lab.ccvisu.graph.GraphVertex.Shape;
import org.sosy_lab.ccvisu.graph.GraphicLayoutInfo;
import org.sosy_lab.ccvisu.graph.Position;

/**
 * Writer for graphical output of layout data.
 */
public abstract class WriterDataLayout extends WriterData {

  protected final Options options;

  /**
   * @param graph       Graph representation, contains the positions of the vertices.
   */
  public WriterDataLayout(PrintWriter out, GraphData graph, Options options) {
    super(out, graph);
    this.options = options;
  }

  /**
   * Writes the layout data in a graphics format.
   */
  @Override
  abstract public void write();

  public GraphicLayoutInfo calculateOffsetAndScale(Dimension size) {
    boolean considerAuxVertices = options.getOption(OptionsEnum.considerAuxVertices).getBool();

    Position minPos = Position.min(graph.getVertices(), true, considerAuxVertices);
    Position maxPos = Position.max(graph.getVertices(), true, considerAuxVertices);

    float width;
    float height;

    final int OFFSET_FROM_BORDERS = calculateVertexRadius(options.graph.getMaxDegree());

    if (options.getOption(OptionsEnum.stretchGraphToBounds).getBool()) {
      width = maxPos.x - minPos.x;
      height = maxPos.y - minPos.y;
    } else {
      width = Position.width(maxPos, minPos);
      height = width;
    }

    if (width == 0) {
      width = 1;
    }
    if (height == 0) {
      height = 1;
    }

    int sizeX = (int) size.getWidth();
    int sizeY = (int) size.getHeight();

    Position offset = new Position();
    offset.x = Position.add(Position.mult(minPos, -1), 0.05f * width).x;
    offset.y = Position.add(Position.mult(minPos, -1), 0.05f * height).y;

    // Flip y-coordinate.
    Position scale = new Position(1, -1, 1);
    scale.x *= (0.9f * sizeX / width);
    if (options.getOption(OptionsEnum.stretchGraphToBounds).getBool()) {
      scale.y *= (0.9f * sizeY / (height + OFFSET_FROM_BORDERS));
    } else {
      scale.y *= (0.9f * sizeY / height);
    }

    Position maxPosScaled = Position.add(maxPos, offset);
    maxPosScaled.mult(scale);

    if (options.getOption(OptionsEnum.stretchGraphToBounds).getBool()) {
      sizeY = (int) Math.abs(maxPosScaled.y) + OFFSET_FROM_BORDERS;
    } else {
      // do nothing
    }

    return new GraphicLayoutInfo(width, height, sizeY, offset, scale, maxPosScaled);
  }

  /**
   * Write graphics layout.
   * @param size  Size of output area (e.g., number of pixel).
   */
  public void writeGraphicsLayout(List<GraphVertex> vertices, List<GraphEdge> edges, int size) {
    writeGraphicsLayout(vertices, edges, new Dimension(size, size));
  }

  public void writeGraphicsLayout(List<GraphVertex> vertices,
      List<GraphEdge> edges, Dimension size) {

    GraphicLayoutInfo graphicLayoutInfo = calculateOffsetAndScale(size);
    int sizeX = (int) size.getWidth();
    int sizeY = (int) size.getHeight();

    // Draw the edges.
    for (GraphEdge edge : edges) {
      if (options.getOption(OptionsEnum.showEdges).getBool() || edge.showEdge()) {
        // Only draw the edge if it and both of its incident vertices are visible
        if ((edge.getSource().isShowVertex() || edge.getTarget().isShowVertex()) && !edge.isAuxiliaryEdge()) {
          Position sourcePos = graphicLayoutInfo.mapToOriginal(edge.getSource().getPosition());
          Position targetPos = graphicLayoutInfo.mapToOriginal(edge.getTarget().getPosition());

          writeEdge(edge, (int) sourcePos.x, (int) sourcePos.y, (int) sourcePos.z,
              (int) targetPos.x, (int) targetPos.y, (int) targetPos.z);
        }
      }
    }

    // Draw the vertices.
    // First draw the vertices that are not annotated (put them to background).
    for (GraphVertex vertex : vertices) {
      if (vertex.isShowVertex() && !(options.getOption(OptionsEnum.hideSource).getBool()
          && vertex.isSource()) && !vertex.isAuxiliary()
          && !vertex.isShowName()) {

        calculateAndWriteVertex(sizeX, sizeY, graphicLayoutInfo, vertex);
      }
    }

    // Draw the annotated vertices.
    // Second draw the annotated vertices (put them to foreground).
    for (GraphVertex vertex : vertices) {
      if (vertex.isShowVertex() && !(options.getOption(OptionsEnum.hideSource).getBool()
          && vertex.isSource()) && !vertex.isAuxiliary()
          && vertex.isShowName()) {

        calculateAndWriteVertex(sizeX, sizeY, graphicLayoutInfo, vertex);
      }
    }
  }

  private void calculateAndWriteVertex(int sizeX, int sizeY,
      GraphicLayoutInfo graphicLayoutInfo, GraphVertex vertex) {

    Position position = graphicLayoutInfo.mapToOriginal(vertex.getPosition());

    // Determine color for vertex.
    Color color = vertex.getColor();
    if (options.getOption(OptionsEnum.depDegreeColor).getBool()) {
      float redValue = vertex.getDegreeOut() / graph.getMaxOutdegree();
      color = new Color(redValue, 1 - redValue, 0);
    }

    if (vertex.getShape() == Shape.FIXED_SIZE_BOX) {
      int radius = 2;
      writeVertex(vertex, (int) position.x, (int) position.y, (int) position.z, radius, color);

    } else if (vertex.getShape() == Shape.FILLER_RECT) {
      int width = Math.max(1, Math.round(sizeX / graphicLayoutInfo.getWidth()));
      int height = Math.max(1, Math.round(sizeY / graphicLayoutInfo.getHeight()));
      writeVertex(vertex, (int) position.x, (int) position.y, (int) position.z, width, height, color);

    } else {
      int radius = calculateVertexRadius(vertex.getDegree());
      writeVertex(vertex, (int) position.x, (int) position.y, (int) position.z, radius, color);
    }
  }

  private int calculateVertexRadius(float degree) {
    return (int) Math.max(Math.pow(degree, 0.5) * options.getOption(OptionsEnum.minVert).getFloat(),
        options.getOption(OptionsEnum.minVert).getFloat());
  }

  /**
   * Writes a vertex.
   *
   * @param vertex     The vertex object, to access vertex attributes.
   * @param xPos       x coordinate of the vertex.
   * @param yPos       y coordinate of the vertex.
   * @param zPos       z coordinate of the vertex.
   * @param radius     Radius of the vertex.
   */
  public void writeVertex(GraphVertex vertex, int xPos, int yPos, int zPos,
      int radius, Color color) {
    writeVertex(vertex, xPos, yPos, zPos, radius, radius, color);
  }

  abstract public void writeVertex(GraphVertex vertex, int xPos, int yPos,
      int zPos, int width, int height, Color color);

  /**
   * Writes an edge.
   *
   * @param edge        an edge in graph.edges
   * @param xPos1       x coordinate of the first point.
   * @param yPos1       y coordinate of the first point.
   * @param zPos1       z coordinate of the first point.
   * @param xPos2       x coordinate of the second point.
   * @param yPos2       y coordinate of the second point.
   * @param zPos2       z coordinate of the second point.
   */
  abstract public void writeEdge(GraphEdge edge, int xPos1, int yPos1, int zPos1,
      int xPos2, int yPos2, int zPos2);

  /**
   * Calculates the two angled lines that form the arrow head.
   */
  protected int[] paintArrow(int x0, int y0, int x1, int y1) {

    int[] arrowPoints = new int[8];

    double angleFactor = 0.5;
    double tipLocPercentage = 0.9 - options.getOption(OptionsEnum.minVert).getFloat() * 0.02;
    double xMid = x0 + tipLocPercentage * (x1 - x0);
    double yMid = y0 + tipLocPercentage * (y1 - y0);

    double deltaX = xMid - x0;
    double deltaY = yMid - y0;
    deltaX *= angleFactor;
    deltaY *= angleFactor;
    double diaLength = (Math.sqrt(deltaX * deltaX + deltaY * deltaY) * Math.sqrt(2));

    // (deltaY, -deltaX)
    double x2 = (x0 + deltaY - xMid) / diaLength;
    double y2 = (y0 - deltaX - yMid) / diaLength;

    // (-deltaY, deltaX)
    double x3 = (x0 - deltaY - xMid) / diaLength;
    double y3 = (y0 + deltaX - yMid) / diaLength;

    double arrowTipSizeFactor = 5;
    double xt2 = xMid + arrowTipSizeFactor * x2;
    double yt2 = yMid + arrowTipSizeFactor * y2;
    double xt3 = xMid + arrowTipSizeFactor * x3;
    double yt3 = yMid + arrowTipSizeFactor * y3;

    arrowPoints[0] = (int) Math.round(xMid);
    arrowPoints[1] = (int) Math.round(yMid);

    arrowPoints[2] = (int) Math.round(xt2);
    arrowPoints[3] = (int) Math.round(yt2);

    arrowPoints[4] = (int) Math.round(xMid);
    arrowPoints[5] = (int) Math.round(yMid);

    arrowPoints[6] = (int) Math.round(xt3);
    arrowPoints[7] = (int) Math.round(yt3);

    return arrowPoints;
  }

  /**
   * @return the options
   */
  public Options getOptions() {
    return options;
  }
}
